昨天(Day 12)我們完成了 UDP Header 解析與 Port Lookup:
Ethernet (L2) ──► IPv4 (L3) ──► UDP (L4) ──► Port Lookup
我們的 Network Stack 已經能夠辨識出「這個 UDP 封包是要交給哪個 Port(例如 8080)」。
然而昨天我們只解析了 8 Bytes 的 UDP 標頭就停下了腳步,封包內包覆的**真正資料(Payload)**完全沒有被提取與呈現。
今天(Day 13),我們正式打通了作業系統核心與使用者空間應用程式的最後一哩路:
Ethernet (L2) ──► IPv4 (L3) ──► UDP (L4) ──► Payload 提取 ──► Application Callback
當外部客戶端送出字串資料時,我們的網路堆疊成功將資料遞交給註冊在 8080 Port 的應用程式,在螢幕上印出:
=== UDP APP ===
hello world
===============
length 包含 8 Bytes Header + Payload,並透過 ntohs() 轉換計算出精確的 payload_len。(uint8_t *)udp + sizeof(struct udp_hdr) 準確定位應用層資料的記憶體起始位址。typedef void (*udp_handler_t)(const uint8_t *data, size_t len);
struct udp_socket 與 udp_bind(),實現 Kernel 與 Application 的架構解耦。udp_echo_app):模擬真實 Daemon 服務,在主程式啟動時向核心註冊監聽 Port 8080。hello world\n(12 bytes)。UDP 的資料單元(Datagram)由兩部分組成:
+-----------------------+-----------------------------+
| UDP Header (8 Bytes) | Payload (應用層真實資料) |
+-----------------------+-----------------------------+
▲ ▲
│ │
udp udp + sizeof(struct udp_hdr)
在 UDP 標頭中,length 欄位定義如下:
8(代表 Payload 為 0,純空封包)。因此計算 Payload 長度的公式為:
uint16_t udp_len = ntohs(udp->length);
// 防禦性檢查:長度不能小於標頭本身
if (udp_len < sizeof(struct udp_hdr)) {
return;
}
size_t payload_len = udp_len - sizeof(struct udp_hdr);
const uint8_t *udp_payload = (const uint8_t *)udp + sizeof(struct udp_hdr);
在現代作業系統中,網路協定堆疊(Kernel Space)與應用程式(User Space)必須嚴格職責分離:
+-------------------------------------------------------------+
| Application Layer |
| [udp_echo_app] [DNS Server (53)] [Game Server (9000)]|
+-------------------------------------------------------------+
▲
Callback 回呼
│
+-------------------------------------------------------------+
| Transport Layer (UDP) |
| Socket Table: Port 8080 ──► Handler 指標 |
+-------------------------------------------------------------+
| Network Layer (IPv4) |
+-------------------------------------------------------------+
| Data Link Layer (Ethernet) |
+-------------------------------------------------------------+
udp_receive() 裡處理業務邏輯?struct udp_socket {
uint16_t port;
udp_handler_t handler; // 登記該 Port 的聯絡窗口
};
當應用程式呼叫 udp_bind(8080, udp_echo_app),就是告訴核心:「如果有送往 8080 的封包,請回呼我的 udp_echo_app 函式!」| Web API 路由(High-Level) | 網路傳輸層(Low-Level) |
|---|---|
Route Path(例如 /api/v1/echo) |
Port 號碼(例如 8080) |
| Request Body(JSON / Text) | UDP Payload(原始位元組資料) |
| Controller / Handler 函式 | udp_handler_t Callback 函式 |
include/udp.h:Callback 型態與 Socket 結構升級#ifndef UDP_H
#define UDP_H
#include <stdint.h>
#include <stddef.h>
#define UDP_HEADER_LEN 8
#define MAX_UDP_SOCKETS 16
/* UDP Header (RFC 768) - 固定 8 Bytes */
struct udp_hdr
{
uint16_t src_port;
uint16_t dst_port;
uint16_t length;
uint16_t checksum;
} __attribute__((packed));
/* 應用程式回呼函式型態:接收資料指標與資料長度 */
typedef void (*udp_handler_t)(const uint8_t *data, size_t len);
/* UDP Socket 結構 */
struct udp_socket
{
uint16_t port; // 監聽的 Port
udp_handler_t handler; // 收到資料時要執行的 Callback 函式
};
/* 函式宣告 */
void udp_init(void);
int udp_bind(uint16_t port, udp_handler_t handler);
struct udp_socket *udp_lookup(uint16_t port);
void udp_print_header(const struct udp_hdr *udp);
void udp_dump_payload(const uint8_t *payload, size_t len);
void udp_receive(int fd, const uint8_t *buffer, size_t len);
#endif /* UDP_H */
src/udp.c:Payload 解析與應用程式交付#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include "ethernet.h"
#include "ipv4.h"
#include "udp.h"
static struct udp_socket udp_table[MAX_UDP_SOCKETS];
void udp_init(void)
{
memset(udp_table, 0, sizeof(udp_table));
}
int udp_bind(uint16_t port, udp_handler_t handler)
{
if (port == 0) return -1;
if (udp_lookup(port) != NULL) return -1;
for (int i = 0; i < MAX_UDP_SOCKETS; i++) {
if (udp_table[i].port == 0) {
udp_table[i].port = port;
udp_table[i].handler = handler;
printf("[UDP] Bound to port %u\n", port);
return 0;
}
}
return -1;
}
struct udp_socket *udp_lookup(uint16_t port)
{
for (int i = 0; i < MAX_UDP_SOCKETS; i++) {
if (udp_table[i].port == port) {
return &udp_table[i];
}
}
return NULL;
}
void udp_dump_payload(const uint8_t *payload, size_t len)
{
printf("Payload (%zu bytes):\n", len);
for (size_t i = 0; i < len; i++) {
putchar(payload[i]);
}
printf("\n");
}
void udp_receive(int fd, const uint8_t *frame, size_t len)
{
(void)fd;
if (len < ETH_HEADER_LEN + sizeof(struct ipv4_hdr) + sizeof(struct udp_hdr)) {
return;
}
const uint8_t *payload = frame + ETH_HEADER_LEN;
const struct ipv4_hdr *ip = (const struct ipv4_hdr *)payload;
uint8_t ihl = (ip->version_ihl & 0x0F) * 4;
if (len < ETH_HEADER_LEN + ihl + sizeof(struct udp_hdr)) {
return;
}
const struct udp_hdr *udp = (const struct udp_hdr *)(payload + ihl);
// 1. 印出 UDP Header
udp_print_header(udp);
// 2. 取得目的 Port 與查表
uint16_t dst_port = ntohs(udp->dst_port);
struct udp_socket *sock = udp_lookup(dst_port);
// 3. 提取 Payload 與計算長度
uint16_t udp_len = ntohs(udp->length);
if (udp_len < sizeof(struct udp_hdr)) {
return;
}
size_t payload_len = udp_len - sizeof(struct udp_hdr);
const uint8_t *udp_payload = (const uint8_t *)udp + sizeof(struct udp_hdr);
// 4. 交付至應用程式(Deliver)
if (sock) {
printf("[UDP] Deliver to Port %u\n", dst_port);
udp_dump_payload(udp_payload, payload_len);
if (sock->handler) {
sock->handler(udp_payload, payload_len);
}
} else {
printf("[UDP] No listener\n");
}
fflush(stdout);
}
src/tap.c:應用程式定義與註冊/* 第一個 UDP 應用程式:收到什麼就印出什麼 */
void udp_echo_app(const uint8_t *data, size_t len)
{
printf("\n=== UDP APP ===\n");
// fwrite 可以精確輸出 len 長度,不怕資料中包含 '\0'
fwrite(data, 1, len, stdout);
printf("===============\n\n");
fflush(stdout);
}
int main()
{
...
udp_init();
udp_bind(8080, udp_echo_app); // 將 8080 Port 綁定給 echo app
...
}
make network
sudo ./network
sudo ip addr add 10.0.0.1/24 dev tap0 2>/dev/null || true
sudo ip link set tap0 up
# 送出測試字串
echo "hello world" | nc -u -w 1 10.0.0.2 8080
[ACCEPT]
Ethernet Frame
-------------------------
Destination : 02:00:00:00:00:01
Source : c6:bf:60:33:9d:73
EtherType : 0x0800
IPv4 Packet
------------------
Version : 4
Header Length: 20 bytes
TTL : 64
Protocol : 17
Checksum : 0x6797 (OK)
Source IP : 10.0.0.1
Destination IP : 10.0.0.2
[IPv4] Local Delivery (for me)
UDP Packet
-------------------
Source Port : 49000
Destination Port : 8080
Length : 20
Checksum : 0x7af2
[UDP] Deliver to Port 8080
Payload (12 bytes):
hello world
=== UDP APP ===
hello world
===============
Frame length: 54 bytes
| 協定層級 | 欄位 / 內容 | 長度 | 累計長度 | 說明 |
|---|---|---|---|---|
| L2 Ethernet | MAC Header | 14 Bytes | 14 Bytes | 目的 MAC (02:00:00:00:00:01) + 來源 MAC + EtherType 0x0800 |
| L3 IPv4 | IP Header | 20 Bytes | 34 Bytes | IHL=5 (20 bytes), Protocol=17 (UDP), 10.0.0.1 ➔ 10.0.0.2 |
| L4 UDP | UDP Header | 8 Bytes | 42 Bytes | Src=49000, Dst=8080, Length=20 |
| L7 Application | Payload 資料 | 12 Bytes | 54 Bytes | 字串 "hello world\n"(11 個字母 + 1 個換行字元) |
到今天為止,我們完成的網路堆疊已經具備完整的向下與向上傳輸鏈路:
+------------------------------------+
| Application | <-- udp_echo_app (收到了 "hello world")
+------------------------------------+
▲
+------------------------------------+
| UDP Socket | <-- udp_table (8080 -> handler)
+------------------------------------+
▲
+------------------------------------+
| UDP | <-- 解析 Port、驗證 Length、提取 Payload
+------------------------------------+
▲
+------------------------------------+
| IPv4 | <-- 路由轉發與本機交付決策 (Local Delivery)
+------------------------------------+
▲
+------------------------------------+
| ARP | <-- IP 與 MAC 映射快取
+------------------------------------+
▲
+------------------------------------+
| Ethernet | <-- 訊框篩選與 EtherType 分流
+------------------------------------+
▲
+------------------------------------+
| TAP Device | <-- 虛擬網路介面 (tap0)
+------------------------------------+
完成了接收路徑(Receive Path)後,明天我們將打造逆向的傳送路徑(Transmit Path):
Application ──► UDP ──► IPv4 ──► ARP Lookup ──► Ethernet ──► TAP
你將會親手實作:
write() 透過 TAP 設備送出真實 UDP 封包
nc -lu 9999 成功接收來自你的自製 Network Stack 發出的問候!